今天的內容是基於 day26 與 day27 之上的,day26 講的是與資料庫的互動,day27 則是關於 jwt 的概念,所以如果覺得講的內容有所缺失,可以往前面幾篇看一下
整個程式碼的架構如下圖所示:

今天主要是實作 jwt 的運作原理,在 jwt 的設定中,可以設定有效的持續時間,表示使用者成功登入後,產生一組 jwt token,回傳給前端,再由前端存取 jwt 至 storage,但因為目前我們是做伺服器端,所以做到回傳 jwt 給前端,因此我們產生 jwt 的位置會放在使用者成功登入後
// controllers/user/handler.go
// // 使用者登入
// // 如果資料庫中沒有找到對應的使用者帳密,回傳 err = record not found,有找到則 err = nil
// func LoginUser(email, password string) string {
// 	passwordSha1 := sha1It(password)
// 	user, err := User.LoginUser(email, passwordSha1)
// 	if err != nil {
// 		return "wrong email or password"
// 	}
	jwt, err := mJwt.GenerateJWT(user.Id, user.Email)
 	if err != nil {
 		fmt.Println("jwt error:", err)
 	}
 	return jwt
// }
// // 加密字串
var Secret = os.Getenv("SECRET")
// // sha加密
// func sha1It(password string) string {
// 	h := sha1.New()
// 	h.Write([]byte(password))
	// bs := h.Sum([]byte(Secret))
// 	encryptCode := fmt.Sprintf("%x", bs)
// 	return encryptCode
// }
實作關於 jwt 的部分就放在 middleware 的資料夾底下,首先產生 jwt 的部分主要是定義資料封包的樣子,在這邊,我們定義的資訊在 AuthClaims 與 GenerateJWT 中:
此外實作解析 jwt 的方法 (JWTAuth) 以及取得 jwt 中的資訊 (GetUserId),這兩個的流程十分相似,基本上就是先確認這個 jwt 的正確性與是否在有效期限內,在 parseToken 函式中, expiresAt := time.Now().Add(10 * time.Second).Unix() 表示這個 token 的有效時間為 10 秒鐘,從產生 jwt 的那一刻開始計算,超過 10 秒後則過期
// middleware/jwt.go
package jwt
import (
	"fmt"
	"net/http"
	"os"
	"time"
	t "it/day28/app/utils/template"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
	_ "github.com/joho/godotenv/autoload"
)
var Secret = os.Getenv("SECRET")
var Issuer = os.Getenv("Issuer")
type AuthClaims struct {
	jwt.StandardClaims
	Email string
	Id    int
}
// 解析 jwt
func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 通過 http header 中的 token 解析認證
		token := c.Request.Header.Get("token")
		if token == "" {
			t.Template(c, http.StatusBadRequest, "not jwt token")
			c.Abort()
			return
		}
		// 解析 jwt 是否正確,如果不正確則提前結束,正確就繼續
		_, err := parseToken(token)
		if err != nil {
			var errMsg string
			if ve, ok := err.(*jwt.ValidationError); ok {
				if ve.Errors&jwt.ValidationErrorMalformed != 0 {
					errMsg = "token無效"
				} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
					errMsg = "token過期"
				}
			}
			t.Template(c, http.StatusBadRequest, errMsg)
			c.Abort()
			return
		}
	}
}
// 獲取使用者資訊
func GetUserInfo(c *gin.Context) (*AuthClaims, error) {
	// 通過http header中的token解析來認證
	token := c.Request.Header.Get("token")
	if token == "" {
		return nil, fmt.Errorf("no jwt token")
	}
	claim, err := parseToken(token)
	if err != nil {
		return nil, fmt.Errorf("bad jwt: %s", err)
	}
	return claim, nil
}
// 解析 jwt token
func parseToken(token string) (*AuthClaims, error) {
	jwtToken, err := jwt.ParseWithClaims(token, &AuthClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return []byte(Secret), nil
	})
	if err == nil && jwtToken != nil {
		if claim, ok := jwtToken.Claims.(*AuthClaims); ok && jwtToken.Valid {
			return claim, nil
		}
	}
	return nil, err
}
// 產生 jwt
func GenerateJWT(id int, email string) (string, error) {
	expiresAt := time.Now().Add(10 * time.Second).Unix()
	claims := AuthClaims{
		Id:    id,
		Email: email,
		StandardClaims: jwt.StandardClaims{
			Issuer:    Issuer,
			ExpiresAt: expiresAt,
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(Secret))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}
環境變數定義如下:
// .env
DBUSER="root"
DBPASSWORD="1A2B3c4d"
DBHOST="127.0.0.1"
DBPORT="3306"
DBNAME="test_database"
SECRET="it"
Issuer="ithome"
另外新開一支驗證與取得使用者資訊的 api 來確認我們 jwt 的實作
// api/go
// 獲取 jwt token 中的資訊
func ApiGetUserInfo(c *gin.Context) {
	claim, err := mJwt.GetUserInfo(c)
	if err != nil {
		t.Template(c, http.StatusBadRequest, err)
	}
	t.Template(c, http.StatusOK, claim)
}
user.Use(mJwt.JWTAuth()) 表示在執行下面的 user.GET("/info", v1.ApiGetUserInfo) 前,會先執行 user.Use(mJwt.JWTAuth()),也是所謂的 middleware,middleware 可以當作因為這個函式很常會使用到,所以特別做成一包函式,透過 Use 的方式打包,這樣就可以避免下面假設有一堆的 api 都會用到,不斷的在各自 api 中重複引用這個函式
// router.go
//func InitRouter() *gin.Engine {
//	r := gin.Default()
//	// 定義前端打的 api 路徑
//	r.POST("/v1/register", v1.ApiRegister)
//	r.POST("/v1/login", v1.ApiLogin)
	// 需要用到 jwt 的打包在一起
	user := r.Group("/v1/user")
	user.Use(mJwt.JWTAuth())
	user.GET("/info", v1.ApiGetUserInfo)
//	return r
//}
那接下來我們就可以看看結果,首先是成功登入獲得 jwt 的畫面:

我們可以先透過 解析 jwt 網站看一下我們獲得的 jwt 結果如何:

當取得 jwt ,我們將其複製放在 http 的 header 中,如果正確及在有效時間內的結果如下:

如果過期的 jwt 則如下:

https://github.com/luckyuho/ithome30-golang/tree/main/day28